﻿#region Using Statements
using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
#endregion

namespace GameBase.Particles
{
    /// <summary>
    /// Basic properties needed to implement any 2D particle system.
    /// </summary>
    public class ParticleSystem2D : IDisposable
    {
        #region Fields

        SpriteBatch SpriteBatch;

        // maximum number of particles displayed at one time
        int maxParticles;

        // stores the particles... circular queue
        Particle2D[] particleArray;

        // regions within the circular queue...
        // ... pointers to the first particle of that region
        int ptrActive;
        int ptrFree;

        float currentTime;

        static Random Random = new Random();

        #endregion

        #region Position

        /// <summary>
        /// Gets or sets the position of the particle system.
        /// </summary>
        public Vector2 Position { get; set; }

        #endregion

        #region Particles

        // backing fields
        Texture2D texture;
        Vector2 texture_origin;

        /// <summary>
        /// Sets the texture used by the particles of the system.
        /// </summary>
        public Texture2D Texture
        {
            set
            {
                texture = value;
                texture_origin.X = texture.Width / 2;
                texture_origin.Y = texture.Height / 2;
            }
        }

        /// <summary>
        /// Sets the duration of the particles of the system.
        /// </summary>
        public TimeSpan Duration
        {
            private get;
            set;
        }

        #endregion

        #region Velocity

        /// <summary>
        /// Sets the maximum velocity given for any particle.
        /// </summary>
        public float MaxVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum velocity given for any particle.
        /// </summary>
        public float MinVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the factor which controls how the velocity of any particle will change during its life.
        /// <para>A value of 1 will make particles keep going at the same velocity as when they were created.</para>
        /// <para>A value of 0 will make particles completely stop before they disappear.</para>
        /// <para>Values greater than 1 will make particles speed up over time.</para>
        /// </summary>
        public float EndVelocity
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the direction and strength of the gravity force applied to the particles of the system.
        /// </summary>
        public Vector2 Gravity
        {
            private get;
            set;
        }

        #endregion

        #region Color, Size

        /// <summary>
        /// Sets the maximum color value given for any particle.
        /// </summary>
        public Color MaxColor
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum color value given for any particle.
        /// </summary>
        public Color MinColor
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the maximum start size given for any particle.
        /// </summary>
        public float MaxStartSize
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum start size given for any particle.
        /// </summary>
        public float MinStartSize
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the maximum end size given for any particle.
        /// </summary>
        public float MaxEndSize
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the minimum end size given for any particle.
        /// </summary>
        public float MinEndSize
        {
            private get;
            set;
        }

        /// <summary>
        /// Sets the blend state used to draw the ParticleSystem3D.
        /// </summary>
        public BlendState BlendState
        {
            private get;
            set;
        }

        #endregion

        #region Initialization

        /// <summary>
        /// Initializes a new instance of this class.
        /// </summary>
        /// <param name="spriteBatch">The SpriteBatch associated to this ParticleSystem2D.</param>
        /// <param name="maxParticles">The maximum number of particles displayed at one time.</param>
        public ParticleSystem2D(SpriteBatch spriteBatch, int maxParticles)
        {
            this.SpriteBatch = spriteBatch;
            this.maxParticles = maxParticles;

            ParticleArray();

            Duration = TimeSpan.FromSeconds(1);
            MaxVelocity = 0.0f;
            MinVelocity = 0.0f;
            Gravity = Vector2.Zero;
            EndVelocity = 1;

            MaxColor = Color.White;
            MinColor = Color.White;
            MaxStartSize = 100;
            MinStartSize = 100;
            MaxEndSize = 100;
            MinStartSize = 100;
            BlendState = BlendState.NonPremultiplied;
        }

        /// <summary>
        /// Initializes particle data.
        /// </summary>
        private void ParticleArray()
        {
            particleArray = new Particle2D[maxParticles];

            for (int i = 0; i < maxParticles; i++)
            {
                particleArray[i] = new Particle2D();
            }
        }

        #endregion
        
        #region Dispose

        // dispose flag
        bool isDisposed = false;

        /// <summary>
        /// Immediately releases all resources used by this object.
        /// </summary>
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Allows an object to try to free resources and perform other cleanup operations before it is reclaimed by garbage collection.
        /// </summary>
        ~ParticleSystem2D()
        {
            Dispose(false);
        }

        /// <summary>
        /// Releases the unmanaged resources used by this object and optionally releases the managed resources.
        /// </summary>
        /// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (!isDisposed)
            {
                if (disposing)
                {
                    if (BlendState != null) { BlendState.Dispose(); BlendState = null; }
                }

                isDisposed = true;
            }
        }

        #endregion

        #region Update

        /// <summary>
        /// Called when the ParticleSystem2D needs to be updated.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public void Update(GameTime gameTime)
        {
            currentTime += (float)gameTime.ElapsedGameTime.TotalSeconds;

            FreeActiveParticles();

            // active region empty... reset timer
            if (ptrActive == ptrFree)
                currentTime = 0;
        }

        /// <summary>
        /// Frees active particles once passed their duration.
        /// </summary>
        private void FreeActiveParticles()
        {
            float duration = (float)Duration.TotalSeconds;

            while (ptrActive != ptrFree)
            {
                float age = currentTime - particleArray[ptrActive].TimeStamp;

                if (age < duration)
                    break;

                // free particle
                ptrActive = (ptrActive + 1) % maxParticles;
            }
        }

        #endregion

        #region Draw

        /// <summary>
        /// Called when the ParticleSystem2D needs to be drawn.
        /// </summary>
        /// <param name="gameTime">Time elapsed since the last call to this method.</param>
        public void Draw(GameTime gameTime)
        {
            SpriteBatch.Begin(SpriteSortMode.Deferred, BlendState);

            if (ptrActive < ptrFree)
            {
                for (int i = ptrActive; i < ptrFree; i++)
                {
                    // normalized age [0, 1]
                    float age = currentTime - particleArray[i].TimeStamp;
                    age = MathHelper.Clamp(age / (float)Duration.TotalSeconds, 0.0f, 1.0f);

                    DrawParticle(particleArray[i], age);
                }

            }
            else
            {
                for (int i = ptrActive; i < maxParticles; i++)
                {
                    // normalized age [0, 1]
                    float age = currentTime - particleArray[i].TimeStamp;
                    age = MathHelper.Clamp(age / (float)Duration.TotalSeconds, 0.0f, 1.0f);

                    DrawParticle(particleArray[i], age);
                }

                for (int i = 0; i < ptrFree; i++)
                {
                    // normalized age [0, 1]
                    float age = currentTime - particleArray[i].TimeStamp;
                    age = MathHelper.Clamp(age / (float)Duration.TotalSeconds, 0.0f, 1.0f);

                    DrawParticle(particleArray[i], age);
                }
            }

            SpriteBatch.End();
        }

        /// <summary>
        /// Draws a Particle2D.
        /// </summary>
        /// <param name="particle">The Particle2D.</param>
        /// <param name="age">The age of the Particle2D.</param>
        private void DrawParticle(Particle2D particle, float age)
        {
            Vector2 auxPosition = ComputeParticlePosition(particle, age);
            Color color = ComputeParticleColor(particle, age);
            float rotation = ComputeParticleRotation(particle, age);
            float size = ComputeParticleSize(particle, age);

            SpriteBatch.Draw(texture, Position + auxPosition, null, color, rotation, texture_origin, size, SpriteEffects.None, 0.0f);
        }

        /// <summary>
        /// Computes the position of the Particle2D.
        /// <para>This is relative to the position of the whole system.</para>
        /// </summary>
        /// <param name="particle">The Particle2D.</param>
        /// <param name="age">The age of the Particle2D.</param>
        /// <returns>The position of the Particle2D.</returns>
        private Vector2 ComputeParticlePosition(Particle2D particle, float age)
        {
            Vector2 position, velocity;
            position = particle.Position;
            velocity = particle.Velocity;
            velocity.Normalize();

            float startVelocity = particle.Velocity.Length();
            float endVelocity = startVelocity * EndVelocity;

            // integration of velocity gives distance covered...
            float velocityIntegral = startVelocity * age + (endVelocity - startVelocity) * 0.5f * (float)Math.Pow(age, 2);
            position += velocity * velocityIntegral * (float)Duration.TotalSeconds;

            // gravity...
            position += Gravity * (float)Math.Pow(age, 2) * (float)Duration.TotalSeconds;

            return position;
        }

        /// <summary>
        /// Computes the color of the Particle2D.
        /// </summary>
        /// <param name="particle">The Particle2D.</param>
        /// <param name="age">The age of the Particle2D.</param>
        /// <returns>The color of the Particle2D.</returns>
        private Color ComputeParticleColor(Particle2D particle, float age)
        {
            Vector4 vector = particle.Color.ToVector4();
            vector.W *= age * (1 - age) * (1 - age) * 6.7f;
            return new Color(vector);
        }

        /// <summary>
        /// Computes the rotation of a Particle2D.
        /// </summary>
        /// <param name="particle">The Particle2D.</param>
        /// <param name="age">The age of the Particle2D.</param>
        /// <returns>The rotation of the Particle2D.</returns>
        private float ComputeParticleRotation(Particle2D particle, float age)
        {
            return particle.Rotation * age;
        }

        /// <summary>
        /// Computes the size of a Particle2D.
        /// </summary>
        /// <param name="particle">The Particle2D.</param>
        /// <param name="age">The age of the Particle2D.</param>
        /// <returns>The size of the Particle2D.</returns>
        private float ComputeParticleSize(Particle2D particle, float age)
        {
            return MathHelper.Lerp(particle.StartSize, particle.EndSize, age);
        }

        #endregion

        #region AddParticles

        /// <summary>
        /// Adds a new particle to the system.
        /// </summary>
        /// <param name="position">Starting position of the particle.<para>It's specified relative to the position of the whole system.</para></param>
        /// <param name="velocity">Starting velocity of the particle.</param>
        public void AddParticle(Vector2 position, Vector2 velocity)
        {
            // room for new particles?
            int nextPtrFree = (ptrFree + 1) % maxParticles;
            if (nextPtrFree == ptrActive) return;

            // velocity
            float auxVelocity = MathHelper.Lerp(MinVelocity, MaxVelocity, (float)Random.NextDouble());
            double auxAngle = Random.NextDouble() * MathHelper.TwoPi;
            velocity.X += auxVelocity * (float)Math.Cos(auxAngle);
            velocity.Y += auxVelocity * (float)Math.Sin(auxAngle);

            // rotation
            float rotation = MathHelper.Lerp(-2 * MathHelper.Pi, 2 * MathHelper.Pi, (float)Random.NextDouble());

            // color
            Color color = Color.Lerp(MinColor, MaxColor, (float)Random.NextDouble());

            // size
            float startSize = MathHelper.Lerp(MinStartSize, MaxStartSize, (float)Random.NextDouble());
            float endSize = MathHelper.Lerp(MinEndSize, MaxEndSize, (float)Random.NextDouble());

            // particle
            particleArray[ptrFree].TimeStamp = currentTime;
            particleArray[ptrFree].Position = position;
            particleArray[ptrFree].Velocity = velocity;
            particleArray[ptrFree].Rotation = rotation;
            particleArray[ptrFree].Color = color;
            particleArray[ptrFree].StartSize = startSize;
            particleArray[ptrFree].EndSize = endSize;

            ptrFree = nextPtrFree;
        }

        #endregion
    }
}
